package com.demo.boss.server.impl;

import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.json.JSON;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.demo.boss.common.constants.SysConst;
import com.demo.boss.common.constants.type.SysException;
import com.demo.boss.common.enums.PublicStatus;
import com.demo.boss.common.enums.SysCode;
import com.demo.boss.entity.PmsMenu;
import com.demo.boss.entity.PmsOperator;
import com.demo.boss.mapper.PmsMenuMapper;
import com.demo.boss.mapper.PmsOperatorMapper;
import com.demo.boss.models.dto.OperatorLoginDto;
import com.demo.boss.models.menus.MenuItem;
import com.demo.boss.models.menus.SubMenu;
import com.demo.boss.models.request.OperatorLoginRequest;
import com.demo.boss.server.OperatorService;
import com.demo.boss.tools.JWTUtils;
import com.demo.boss.tools.RedisHelper;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.TimeUnit;

@Service
public class OperatorServiceImpl implements OperatorService {
    @Resource
    private PmsOperatorMapper pmsOperatorMapper;
    @Resource
    private PmsMenuMapper pmsMenuMapper;
    @Resource
    private RedisHelper redisHelper;
    private Object lockObject = new Object();

    @Override
    public OperatorLoginDto login(OperatorLoginRequest loginRequest) {
        // 从数据库获取 login_name对应的用户，取出salt值，计算密码和loginRequest中密码比较
        PmsOperator operator = pmsOperatorMapper.selectOne(new QueryWrapper<PmsOperator>()
                .eq("login_name", loginRequest.getLoginName()));
        // 用户不存在
        if (operator == null) {
            throw new SysException(SysCode.OPERATOR_LOGIN_NAME_ERROR);
        }
        // 查看用户状态 禁用不让登录
        String status = operator.getStatus();
        if (PublicStatus.DISABLE.equals(status)) {
            throw new SysException(SysCode.OPERATOR_DISABLE_ERROR);
        }

        // 取出 salt 值  [数据库中的密码 = md5 (前端提交的密码 + salt )]
        String salt = operator.getSalt();
        String requestPwd = DigestUtil.md5Hex(loginRequest.getLoginPwd() + salt);
        // 调试代码，添加到后端login方法中
        System.out.println("计算后的密码: " + DigestUtil.md5Hex(loginRequest.getLoginPwd() + salt));
        System.out.println("数据库密码: " + operator.getLoginPwd());
        System.out.println("对比：" + requestPwd.equals(operator.getLoginPwd()));
        if (!requestPwd.equals(operator.getLoginPwd())) {
            throw new SysException(SysCode.OPERATOR_NAME_PWD_ERROR);
        }


        // 登录成功 计算 jwt
        String jwtStr = JWTUtils.createJWT(String.valueOf(operator.getId()), 120);
        // 存储 jwt 到 redis
        String jwtKey = SysConst.OPERATOR_JWT_TOKEN + operator.getId();
        Map<String, String> jwtDate = new HashMap<>();
        jwtDate.put("jwt", jwtStr);
        jwtDate.put("type", operator.getType());
        jwtDate.put("loginTime", new SimpleDateFormat("yyyyMMddHHmss").format(new Date()));
        redisHelper.setHashAll(jwtKey, jwtDate, TimeUnit.MINUTES, 120);

        return new OperatorLoginDto(operator.getId(), operator.getShowName(), jwtStr);
    }

    /**
     * 缓存数据到 redis
     * 1、从redis获取查询
     * 2、如果redis没用数据，更新数据库查询
     * 3、将从数据库查询的数据存储到 redis (String类型 --》 JSON )
     */
    @Override
    public List<SubMenu> queryMenusByOperatorId(Long operatorId) {
        List<SubMenu> subMenuList = null;
        try {
            subMenuList = new ArrayList<>();
            String menuKey = SysConst.OPERATOR_MENU + operatorId; // redis存放的key
            String menuJsonStr = redisHelper.getVal(menuKey); // 根据 key 查询 val
            if (StrUtil.isNotBlank(menuJsonStr)){
                // 有值 将 redis 中的json转换List<SubMenu>
                return JSONUtil.toList(menuJsonStr,SubMenu.class);
            }
            /**
             * 防止多线程并发时重复查询数据库。
             * 当缓存（Redis）中没有菜单数据时，多个线程可能同时进入 “查询数据库” 的逻辑。通过synchronized (lockObject)加锁后，同一
             * 时间只有一个线程能执行getSubMenusFromDB(operatorId)和缓存写入操作，其他线程会阻塞等待。等第一个线程查询数据库并将结果
             * 写入 Redis 后，后续线程会从 Redis 中读取数据，避免了重复查询数据库的 “缓存穿透” 问题。
             */
            synchronized (lockObject){
                // redis没菜单数据，查询数据库
                subMenuList = getSubMenusFromDB(operatorId);
                redisHelper.setValue(menuKey,JSONUtil.toJsonStr(subMenuList));
            }

        } catch (Exception e) {
            e.printStackTrace();
            subMenuList = getSubMenusFromDB(operatorId); // 出现异常的补救措施（从数据库拿数据）
        }
        return subMenuList;
    }

    /**
     * 从数据库读取菜单
     */
    private List<SubMenu> getSubMenusFromDB(Long operatorId) {
        List<SubMenu> subMenuList = new ArrayList<>();
        // 查询所有 subMenu
        List<PmsMenu> pmsMenusList = pmsMenuMapper.selectSubMenu(operatorId);
        // 循环每个 subMenu的子菜单
        for (PmsMenu pmsMenu : pmsMenusList) {
            // 查询结果封装为 List<SubMenu>
            SubMenu subMenu = new SubMenu();
            subMenu.setId(pmsMenu.getId());
            subMenu.setName(pmsMenu.getName());
            subMenu.setNumber(pmsMenu.getNumber());
            subMenu.setUrl(pmsMenu.getUrl());

            List<MenuItem> itemList = new ArrayList<>();

            // 查询子菜单
            List<PmsMenu> itemPmsMenuList = pmsMenuMapper.selectMenulItemByParentId(pmsMenu.getId());
            for (PmsMenu menu : itemPmsMenuList) {
                MenuItem menuItem = new MenuItem();
                menuItem.setId(menu.getId());
                menuItem.setName(menu.getName());
                menuItem.setNumber(menu.getNumber());
                menuItem.setUrl(menu.getUrl());
                itemList.add(menuItem); // 添加到集合
            }
            subMenu.setItemList(itemList);
            subMenuList.add(subMenu);
        }
        return subMenuList;
    }
}
